package aso.fighting_groups;

import sim.engine.SimState;
import sim.engine.Steppable;
import sim.engine.Stoppable;
import sim.util.Bag;
import sim.util.Int2D;

/**
 * Class that implements a group.
 */
class Group implements Steppable {

    /**
     * UID for serialization.
     */
    private static final long serialVersionUID = 3491294023592543740L;

    /**
     * Members of the group.
     */
    private Bag members;
    /**
     * Stops the simulation of the group.
     */
    private Stoppable stopper;
    /**
     * The team in which the group is.
     */
    private int team;

    /**
     * @param theMembers
     *            the members of the group
     */
    public Group(final Bag theMembers) {
        this.members = new Bag(theMembers);
        setTeam(((Agent) theMembers.get(0)).getTeam());
    }

    /**
     * @param food
     *            the food to accept
     */
    public void acceptFood(final double food) {
    	//food is distributed across agents in the group
        double foodForOne = food / members.size();
        for (int i = 0; i < members.size(); ++i) {
            ((Agent) members.get(i)).setCarryingFoodAmount(((Agent) members.get(i)).getCarryingFoodAmount() + foodForOne);
        }
    }

    /**
     * @param member
     *            the member to add
     */
    public void addMember(final Agent member) {
    	//a member joins the group. Food gets redistributed
        double food = 0;
        for (int i = 0; i < members.size(); ++i) {
            food += ((Agent) members.get(i)).getCarryingFoodAmount();
            ((Agent) members.get(i)).setCarryingFoodAmount(((Agent) members.get(i)).getCarryingFoodAmount() * ((double) members.size() / ((double) (members.size() + 1))));
        }
        
        //the right variables are set for the agent for joining the group
        members.add(member);
        member.setGroup(this);
        member.setInGroup(true);
        member.setCarryingFoodAmount(member.getCarryingFoodAmount() + food / members.size());
    }

    /**
     * @param fg
     *            the simulation state
     * @param member
     *            the current member of the group
     * @return the location of the food collected
     */
    private Int2D collectFood(final FightingGroups fg, final Object member) {
        Int2D location;
        location = fg.getAgentGrid()[getTeam()].getObjectLocation(member);
        
        //food can be gathered from the locations of all agents in the group. Found food is distributed across the group.
        if (fg.getFoodGrid().field[location.x][location.y] > 0) {
            acceptFood(fg.getFoodGrid().field[location.x][location.y]);
            fg.getFoodGrid().field[location.x][location.y] = 0;
        }
        return location;
    }

    /**
     * @param fg
     *            the simulation state
     * @param enemies
     *            the enemies
     */
    private void fight(final FightingGroups fg, final Bag enemies) {
    	//if an enemy (group) is nearby, there can be a fight. The enemy (group) is selected randomly
        if (enemies.size() > 0) {
            Agent enemy = (Agent) enemies.get(fg.random.nextInt(enemies.size()));
            
            //fight the enemy's group or the individual enemy if the enemy is not in a group. The winner takes the food of the loser. If the group wins, the food has to be distributed again
            if (enemy.isInGroup()) {
                double groupStrength = enemy.getGroup().getStrength();
        	    //the (group with the) highest strength has a bigger chance of winning
                if (fg.random.nextDouble() < (getStrength() / (getStrength() + groupStrength))) {
                    acceptFood(enemy.getGroup().giveFood());
                } else {
                    enemy.getGroup().acceptFood(giveFood());
                }
            } else {
            	//the (group with the) highest strength has a bigger chance of winning
                if (fg.random.nextDouble() < (getStrength() / (getStrength() + enemy.getStrength()))) {
                    acceptFood(enemy.getCarryingFoodAmount());
                    enemy.setCarryingFoodAmount(0);
                } else {
                    enemy.setCarryingFoodAmount(enemy.getCarryingFoodAmount() + giveFood());
                }
            }
        }
    }

    /**
     * @param fg
     *            the simulation state
     * @param enemies
     *            enemies
     * @param friends
     *            friends
     * @param location
     *            the position
     */
    private void findEnemiesAndFriends(final FightingGroups fg, final Bag enemies, final Bag friends, final Int2D location) {
    	//observe which team members and competitors are close to the group
        for (int t = 0; t < fg.getTeamCount(); ++t) {
            if (t != getTeam()) {
                enemies.addAll(fg.getAgentGrid()[t].getNeighborsMaxDistance(location.x, location.y, 1, true, null, null, null));
            } else {
                friends.addAll(fg.getAgentGrid()[t].getNeighborsMaxDistance(location.x, location.y, 1, true, null, null, null));
            }
        }
    }

    /**
     * @return this group's members
     */
    public Bag getMembers() {
        return members;
    }

    /**
     * @return the size of the group
     */
    public int getSize() {
        return members.size();
    }

    /**
     * @return the strength of the group
     */
    public double getStrength() {
        double strength = 0;

        //to get the total group strength, the individual strengths are summed
        for (int i = 0; i < members.size(); ++i) {
            strength += ((Agent) members.get(i)).getStrength();
        }

        return strength;
    }

    /**
     * @return the team of the group
     */
    public int getTeam() {
        return team;
    }

    /**
     * @return the amount of food that the group had
     */
    public double giveFood() {
        double food = 0;

        //if a fight is lost, all food from all members has to be counted and given away to the winner
        for (int i = 0; i < members.size(); ++i) {
            food += ((Agent) members.get(i)).getCarryingFoodAmount();
            ((Agent) members.get(i)).setCarryingFoodAmount(0);
        }

        return food;
    }

    /**
     * @param x
     *            x coordinate
     * @param y
     *            y coordinate
     * @param fg
     *            the simulation state
     * @return true if there is a member on position (x,y)
     */
    private boolean isAMemberOnPos(final int x, final int y, final FightingGroups fg) {
        Int2D location;
        for (int i = 0; i < members.size(); ++i) {
            location = fg.getAgentGrid()[getTeam()].getObjectLocation(members.get(i));
            if (x == location.x && y == location.y) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param otherGroup
     *            the other group to join
     */
    private void joinWithGroup(final Group otherGroup) {
        double food = 0;
        
        //all food is redistributed across the new group in case of groups merging
        for (int i = 0; i < members.size(); ++i) {
            food += ((Agent) members.get(i)).getCarryingFoodAmount();
            ((Agent) members.get(i)).setCarryingFoodAmount(((Agent) members.get(i)).getCarryingFoodAmount() * (members.size() / (members.size() + (double) otherGroup.members.size())));
        }
        for (int i = 0; i < otherGroup.members.size(); ++i) {
            ((Agent) otherGroup.members.get(i)).setCarryingFoodAmount(food / (members.size() + (double) otherGroup.members.size()));
            ((Agent) otherGroup.members.get(i)).setGroup(this);
        }

        members.addAll(otherGroup.members);
    }

    /**
     * @param fg
     *            the simulation state
     */
    private void move(final FightingGroups fg) {
        int moveToX = -1;
        int moveToY = 0;

        int directionX = 0;
        int directionY = 0;

        //move towards new direction based on smell
        for (int i = 0; i < members.size(); ++i) {

            Int2D location;
            location = fg.getAgentGrid()[getTeam()].getObjectLocation(members.get(i));
            int myx = location.x;
            int myy = location.y;

            for (int x = -1; x < 2; ++x) {
                for (int y = -1; y < 2; ++y) {
                    if (!(x == 0 && y == 0)) {
                        int xx, yy;
                        xx = fg.getAgentGrid()[getTeam()].stx(x + myx); // torodial
                        yy = fg.getAgentGrid()[getTeam()].sty(y + myy); // torodial

                        //a neighbouring coordinate (neighbouring to a member) with a higher smell has a higher chance of being move towards compared to lower smell
                        if (!isAMemberOnPos(xx, yy, fg) && (moveToX == -1 || (fg.getSmellGrid().field[xx][yy] > fg.getSmellGrid().field[moveToX][moveToY]) && fg.random.nextDouble() < (fg.getSmellGrid().field[xx][yy] / (fg.getSmellGrid().field[xx][yy] + fg.getSmellGrid().field[moveToX][moveToY])) || (fg.getSmellGrid().field[xx][yy] == fg.getSmellGrid().field[moveToX][moveToY] && fg.random.nextBoolean()))) { // not uniform, but enough to break up the go-up-and-to-the-left syndrome
                            moveToX = xx;
                            moveToY = yy;
                            directionX = x;
                            directionY = y;
                        }
                    }
                }
            }
        }

        for (int i = 0; i < members.size(); ++i) {
            Agent a = (Agent) members.get(i);

            //the actual movement, every agent moves in the same direction
            a.move(directionX, directionY, fg);

            //give agents the option to leave the group based on starvation, the chance of leaving will change based on how long the agent is starving
            if (fg.random.nextDouble() < (1.0 - 1.0 / (1.0 + a.getStarvingIterations() * fg.getStarvationImportance()))) {
                a.setInGroup(false);
                a.setGroup(null);
            }
        }
    }

    /**
     * @param theMembers
     *            the members to set
     */
    public void setMembers(final Bag theMembers) {
        this.members = theMembers;
    }

    /**
     * @param theStopper
     *            the now stopper to use
     */
    public void setStopper(final Stoppable theStopper) {
        this.stopper = theStopper;
    }

    /**
     * @param theTeam
     *            the team to adopt
     */
    public void setTeam(final int theTeam) {
        this.team = theTeam;
    }

    @Override
    public final void step(final SimState state) {

        FightingGroups fg = (FightingGroups) state;

        //if the group is empty, it no longer has to exist
        if (members.size() == 0) {
            stopper.stop();
            fg.removeGroup(this);
            return;
        }

        Bag enemies = new Bag();
        Bag friends = new Bag();

        //let all members collect food at their location, and let them observe nearby team members and competitors
        for (int i = 0; i < members.size(); ++i) {
            Int2D location = collectFood(fg, members.get(i));
            findEnemiesAndFriends(fg, enemies, friends, location);
        }

        //fight with a nearby competitor, if any
        fight(fg, enemies);

        // Grouping

        //don't join with lonely agents or agents that are already in the group
        Bag friendsWithGroup = new Bag();
        for (int i = 0; i < friends.size(); ++i) {
            if (((Agent) friends.get(i)).isInGroup() && ((Agent) friends.get(i)).getGroup() != this) {
                friendsWithGroup.add(friends.get(i));
            }
        }

        //consider merging with an encountered group from the same team. 
        //the group will only merge another group when starving, the encountered group is not starving, and the chance of merging will change based on how long the group is starving
        if (friendsWithGroup.size() > 0) {
            Agent friend = (Agent) friendsWithGroup.get(fg.random.nextInt(friendsWithGroup.size()));
            if (friend.getStarvingIterations() == 0 && fg.random.nextDouble() < (1.0 - 1.0 / (1.0 + ((Agent) members.get(0)).getStarvingIterations() * fg.getStarvationImportance()))) {
                friend.getGroup().joinWithGroup(this);
                stopper.stop();
                fg.removeGroup(this);
                return;
            }
        }
        
        //move in a direction based on smell
        move(fg);
    }

}
